← Writeups

4 Single endpoint race conditions

Tengo conocimento de que este correo carlos@ginandjuice.shop tiene una invitacion sin reclamar para ser admin, mi objetivo es actualizar mi correo hacia este.

Al hacer uso de la funcionalidad de actualizar correo utilizando mi exploit server, obtuve este mensaje

Please click the link in your email to confirm the change of e-mail to: 
anything@exploit0a6f00e8036ad3f1809e0c4a0138008aexploit-server.net

y obtuve un enlace en mi correo https://0a9d000f0344d3ed80df0de400a3002e.web-security-academy.net/confirm-email?user=wiener&token=7gidkYCg2PFLZvH8 Tambien decidi probar el otro caso: enviar nuevamente otra solicitud de actualizacion de correo y luego probar el enlace anterior, al hacerlo obtuve este mensaje "This link is invalid." por lo que ahora se sabe que el sistema por detras almacena una direccion de correo pendiente a la vez. Al enviar 2 requests de actualizacion en paralelo obtuve 2 enlaces con el mismo timestamp

https://0a9d000f0344d3ed80df0de400a3002e.web-security-academy.net/confirm-email?user=wiener&token=uk55BhkkCexOIIU6
https://0a9d000f0344d3ed80df0de400a3002e.web-security-academy.net/confirm-email?user=wiener&token=uk55BhkkCexOIIU6

La vulnerabilidad ocurre porque el sistema solo mantiene un único email pendiente por usuario en base de datos y el proceso de envío de confirmación no es atómico. Existe una ventana de carrera entre: (1) iniciar la tarea que enviará el correo y (2) recuperar desde la base de datos el email pendiente para renderizar la plantilla. Cuando múltiples solicitudes POST /my-account/change-email se envían en paralelo, una request puede sobrescribir el email pendiente mientras otra ya inició el envío, provocando que el token generado para una dirección termine siendo enviado a otra. Esto genera una race condition por estado compartido mutable, permitiendo colisión y apropiación de confirmaciones (Broken Access Control / Race Condition según OWASP).

# Implementación vulnerable

# POST /my-account/change-email
if valid_session():

    db.pending_email[user] = request.email   # ❌ Sobrescribe valor global

    token = generate_token(user)

    async_send_email(user)   # ❌ Tarea asíncrona

# Worker que envía el correo
function async_send_email(user):
    email = db.pending_email[user]   # ❌ Lee estado mutable compartido
    send_confirmation(email, token)
# Implementación segura (operación atómica)

# POST /my-account/change-email
if valid_session():

    token = generate_token(user)

    db.store_pending_change({
        user: user,
        email: request.email,
        token: token
    })   # Registro independiente, no sobrescribible

    send_confirmation(request.email, token)  # Usa valor inmutable

El problema central no es el paralelismo en sí, sino la dependencia de un único registro mutable que se reutiliza entre requests concurrentes sin aislamiento transaccional.